// MandelbrotExplorer.nova // Interactive Mandelbrot set renderer ported to Nova. // Including Cross-Platform Touch & Trackpad Zoom Support, // and responsive Full Screen / Full Window modes. using namespace lib.emscripten; using namespace lib.math; using namespace lib.opengl; using namespace lib.sdl2; class MandelbrotExplorer { // --- Application State Declarations --- private static double centerX; private static double centerY; private static double zoom; private static double lastZoom; private static int width; private static int height; // --- Display Mode State --- public enum DisplayMode : byte { Canvas, FullWindow, FullScreen } private static DisplayMode displayMode; private static int initialWidth, initialHeight; private static int prevWidth, prevHeight; private static SDL_Area prevWindowSize; private static SDL_Window w; private static SDL_GLContext c; private static uint shaderProgram; private static uint[] vbos; private static bool drag; private static bool isPinching; private static int activeFingers; private static uint lastTapTime; private static bool cycling; private static bool screenshot_requested; private static bool done; private static bool emscriptenActive; private static int mouseX; private static int mouseY; // Uniform locations private static int u_matrix_location; private static int u_time_location; private static int u_max_iter_location; // --- Main Function --- public static void main( String[] args ) { // --- Initialize Attributes --- centerX = 0.0; centerY = 0.0; zoom = 300.0; lastZoom = 300.0; initialWidth = 1000; initialHeight = 600; width = initialWidth; height = initialHeight; prevWidth = 0; prevHeight = 0; drag = false; isPinching = false; activeFingers = 0; lastTapTime = 0; cycling = false; screenshot_requested = false; done = false; mouseX = 0; mouseY = 0; Stream.writeLine( "===================================================" ); Stream.writeLine( " WELCOME TO MANDELBROT EXPLORER " ); Stream.writeLine( "===================================================" ); Stream.writeLine( "Controls:" ); Stream.writeLine( " Left Click + Drag : Pan around the fractal" ); Stream.writeLine( " Mouse Wheel / Pinch : Zoom in and out" ); Stream.writeLine( " Double Click / Tap: Toggle Full Screen" ); Stream.writeLine( " c Key : Toggle Colour Flow" ); Stream.writeLine( " s Key : Save Screenshot" ); Stream.writeLine( " r Key : Reset View to Home" ); Stream.writeLine( " f Key : Toggle Full Screen Mode" ); Stream.writeLine( " w Key : Toggle Full Window Mode (Web)" ); Stream.writeLine( "===================================================" ); Stream.writeLine( "Ready to explore the infinite..." ); emscriptenActive = Emscripten.isActive( ); displayMode = emscriptenActive ? DisplayMode.Canvas : DisplayMode.FullWindow; if ( emscriptenActive ) { Emscripten.disableResizeControl( ); Emscripten.setHideMousePointerCheckbox( false ); Emscripten.setFullScreenChangeCallback( "MandelbrotExplorer", fullScreenModeChange ); Emscripten.setResizeEventCallback( "MandelbrotExplorer", resizeEventCallback ); } // Setup SDL SDL2.SDL_Init( SDL2.SDL_INIT_VIDEO ); SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_CONTEXT_MAJOR_VERSION, 2 ); SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_CONTEXT_MINOR_VERSION, 0 ); SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_CONTEXT_PROFILE_MASK, SDL2.SDL_GL_CONTEXT_PROFILE_ES ); SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_DOUBLEBUFFER, 1 ); SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_DEPTH_SIZE, 24 ); w = SDL2.SDL_CreateWindow( "Mandelbrot Explorer", SDL2.SDL_WINDOWPOS_CENTERED, SDL2.SDL_WINDOWPOS_CENTERED, width, height, SDL2.SDL_WINDOW_OPENGL | SDL2.SDL_WINDOW_RESIZABLE ); if ( w == null ) { Stream.writeLine( "Failed to create window: " + SDL2.SDL_GetError( ) ); return; } c = SDL2.SDL_GL_CreateContext( w ); // Force the Emscripten canvas to match our SDL window size if ( emscriptenActive ) { Emscripten.setCanvasSize( width, height ); // Initialize our native JavaScript trackpad/mouse listener. Emscripten.initNativeZoomListener( ); } initGL( ); if ( emscriptenActive ) { Emscripten.setMainLoop( renderFrame, -1, 1 ); } else { do { renderFrame( ); } while( !done ); } // Cleanup OpenGL.glDeleteProgram( shaderProgram ); OpenGL.glDeleteBuffers( 1, vbos ); SDL2.SDL_GL_DeleteContext( c ); SDL2.SDL_DestroyWindow( w ); SDL2.SDL_Quit( ); } // --- Initialization --- private static void initGL( ) { String vertexShaderSource = "#version 100\n" + "attribute vec3 aPos;\n" + "void main() {\n" + " gl_Position = vec4(aPos, 1.0);\n" + "}\n"; String fragmentShaderSource = "#version 100\n" + "#ifdef GL_ES\n" + "precision highp float;\n" + "precision highp int;\n" + "#endif\n" + "uniform mat4 u_matrix;\n" + "uniform float u_time;\n" + "uniform int u_max_iter;\n" + "void main() {\n" + " vec4 pos = u_matrix * vec4(gl_FragCoord.xy, 0.0, 1.0);\n" + " vec2 c = pos.xy;\n" + " vec2 z = vec2(0.0);\n" + " int iter = 0;\n" + " for (int i = 0; i < 1000; i++) {\n" + " if (i >= u_max_iter) break;\n" + " z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + c;\n" + " if (length(z) > 2.0) break;\n" + " iter++;\n" + " }\n" + " if (iter == u_max_iter) {\n" + " gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);\n" + " } else {\n" + " float t = float(iter) / float(u_max_iter);\n" + " vec3 col = 0.5 + 0.5 * cos(u_time + t * 20.0 + vec3(0.0, 0.6, 1.0));\n" + " gl_FragColor = vec4(col, 1.0);\n" + " }\n" + "}\n"; uint vs = OpenGL.glCreateShader( OpenGL.GL_VERTEX_SHADER ); OpenGL.glShaderSource( vs, vertexShaderSource ); OpenGL.glCompileShader( vs ); uint fs = OpenGL.glCreateShader( OpenGL.GL_FRAGMENT_SHADER ); OpenGL.glShaderSource( fs, fragmentShaderSource ); OpenGL.glCompileShader( fs ); shaderProgram = OpenGL.glCreateProgram( ); OpenGL.glAttachShader( shaderProgram, vs ); OpenGL.glAttachShader( shaderProgram, fs ); // Bind attribute before linking for WebGL 1.0 OpenGL.glBindAttribLocation( shaderProgram, 0, "aPos" ); OpenGL.glLinkProgram( shaderProgram ); OpenGL.glDeleteShader( vs ); OpenGL.glDeleteShader( fs ); u_matrix_location = OpenGL.glGetUniformLocation( shaderProgram, "u_matrix" ); u_time_location = OpenGL.glGetUniformLocation( shaderProgram, "u_time" ); u_max_iter_location = OpenGL.glGetUniformLocation( shaderProgram, "u_max_iter" ); // Setup Geometry (Explicit floats to prevent Nova from creating an integer array!) float[] verts = { 1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 0.0f, -1.0f, -1.0f, 0.0f }; vbos = new uint[ 1 ]; OpenGL.glGenBuffers( 1, vbos ); OpenGL.glBindBuffer( OpenGL.GL_ARRAY_BUFFER, vbos[ 0 ] ); OpenGL.glBufferData( OpenGL.GL_ARRAY_BUFFER, verts, OpenGL.GL_STATIC_DRAW ); // Force the viewport to match the dimensions OpenGL.glViewport( 0, 0, width, height ); } // --- Main Frame Loop --- public static void renderFrame( ) { // --- 1. BROWSER MOUSE WHEEL (Polled from JS Runtime) --- if ( emscriptenActive ) { float zoomDelta = Emscripten.getNativeZoomDelta( ); if ( zoomDelta != 0.0f ) { // Fractal zoom is multiplicative; // scale the delta appropriately. zoom *= ( 1.0 + (double)zoomDelta * 1.2 ); } } SDL_Event e = SDL2.SDL_PollEvent( ); if ( e != null ) { switch ( e.id ) { case SDL2.SDL_WINDOWEVENT: { SDL_WindowEvent weEvent = (SDL_WindowEvent)e; if ( weEvent.event == SDL2.SDL_WINDOWEVENT_RESIZED ) { width = weEvent.data1; height = weEvent.data2; OpenGL.glViewport( 0, 0, width, height ); } break; } case SDL2.SDL_MOUSEBUTTONDOWN: { SDL_MouseButtonEvent mbEvent = (SDL_MouseButtonEvent)e; if ( mbEvent.which == SDL2.SDL_TOUCH_MOUSEID ) break; // Ignore synthetic touch if ( mbEvent.button == SDL2.SDL_BUTTON_LEFT ) { // Double click full screen toggle if ( mbEvent.clicks == 2 ) { if ( emscriptenActive && ( displayMode == DisplayMode.FullScreen ) ) { toggleFullScreenMode( ); return; } if ( emscriptenActive ) toggleFullWindowMode( ); else toggleFullScreenMode( ); return; } drag = true; } break; } case SDL2.SDL_MOUSEBUTTONUP: { SDL_MouseButtonEvent mbEvent = (SDL_MouseButtonEvent)e; if ( mbEvent.which == SDL2.SDL_TOUCH_MOUSEID ) break; // Ignore synthetic touch if ( mbEvent.button == SDL2.SDL_BUTTON_LEFT ) drag = false; break; } case SDL2.SDL_MOUSEMOTION: { SDL_MouseMotionEvent mmEvent = (SDL_MouseMotionEvent)e; if ( mmEvent.which == SDL2.SDL_TOUCH_MOUSEID ) break; // Ignore synthetic touch if ( drag ) { double xrel = mmEvent.x - mouseX; double yrel = mmEvent.y - mouseY; centerX -= ( xrel / zoom ) * 2; centerY += ( yrel / zoom ) * 2; } mouseX = mmEvent.x; mouseY = mmEvent.y; break; } case SDL2.SDL_MOUSEWHEEL: { // --- 2. DESKTOP MOUSE WHEEL (Standard SDL) --- if ( !emscriptenActive ) { SDL_MouseWheelEvent mwEvent = (SDL_MouseWheelEvent)e; zoom *= ( mwEvent.y > 0 ) ? 1.1 : 0.9; } break; } case SDL2.SDL_MULTIGESTURE: { // --- 3. PINCH TO ZOOM (Android, iOS, Surface Touch) --- SDL_MultiGestureEvent mgEvent = (SDL_MultiGestureEvent)e; if ( mgEvent.dDist != 0.0 ) { zoom *= ( 1.0 + (double)mgEvent.dDist * 5.0 ); // Adjust 5.0 for sensitivity } break; } case SDL2.SDL_FINGERDOWN: { activeFingers++; // Double tap full screen toggle uint currentTime = SDL2.SDL_GetTicks(); if ( activeFingers == 1 ) { if ( currentTime - lastTapTime < 300 ) // 300ms threshold { if ( emscriptenActive && ( displayMode == DisplayMode.FullScreen ) ) { toggleFullScreenMode( ); } else if ( emscriptenActive ) { toggleFullWindowMode( ); } else { toggleFullScreenMode( ); } lastTapTime = 0; // Reset } else { lastTapTime = currentTime; } } // Only enable panning if exactly ONE finger is on the screen if ( activeFingers == 1 ) { drag = true; isPinching = false; SDL_TouchFingerEvent tfEvent = (SDL_TouchFingerEvent)e; mouseX = (int)(tfEvent.x * width); mouseY = (int)(tfEvent.y * height); } // If a second finger touches down, kill the pan and start the pinch else if ( activeFingers > 1 ) { drag = false; isPinching = true; } break; } case SDL2.SDL_FINGERUP: { activeFingers--; if ( activeFingers <= 0 ) { activeFingers = 0; drag = false; isPinching = false; } // Note: If you drop from 2 fingers back to 1, drag remains false. // This prevents the camera from suddenly jumping to the remaining finger. // You just lift and touch again to resume panning! break; } case SDL2.SDL_FINGERMOTION: { // ONLY pan if we are officially dragging and NOT pinching if ( drag && !isPinching ) { SDL_TouchFingerEvent tfEvent = (SDL_TouchFingerEvent)e; int currentX = (int)(tfEvent.x * width); int currentY = (int)(tfEvent.y * height); double xrel = currentX - mouseX; double yrel = currentY - mouseY; centerX -= ( xrel / zoom ) * 2; centerY += ( yrel / zoom ) * 2; mouseX = currentX; mouseY = currentY; } break; } case SDL2.SDL_KEYDOWN: { SDL_KeyboardEvent kbEvent = (SDL_KeyboardEvent)e; switch ( kbEvent.sym ) { case SDL2.SDLK_c: cycling = !cycling; Stream.writeLine( "Color Cycling: " + ( cycling ? "ON" : "OFF" ) ); break; case SDL2.SDLK_s: screenshot_requested = true; break; case SDL2.SDLK_r: centerX = 0.0; centerY = 0.0; zoom = 300.0; cycling = false; Stream.writeLine( "View Reset to Home" ); break; case SDL2.SDLK_f: if ( emscriptenActive && ( displayMode == DisplayMode.FullWindow ) ) break; toggleFullScreenMode( ); break; case SDL2.SDLK_w: if ( emscriptenActive && ( displayMode != DisplayMode.FullScreen ) ) { toggleFullWindowMode( ); } break; case SDL2.SDLK_ESCAPE: if ( !emscriptenActive && ( displayMode == DisplayMode.FullScreen ) ) { toggleFullScreenMode( ); } else if ( emscriptenActive && ( displayMode == DisplayMode.FullWindow ) ) { toggleFullWindowMode( ); } else { done = true; } break; } break; } case SDL2.SDL_QUIT: done = true; break; } } // 1. Prepare transformation matrix float[] m = MandelbrotMath.translate( centerX, centerY, 0 ); float[] s = MandelbrotMath.scale( (float)(width / zoom), (float)(height / zoom), 1 ); float[] view = MandelbrotMath.multiply( m, s ); float[] screenScale = MandelbrotMath.scale( 2.0f / width, 2.0f / height, 1 ); float[] screenTrans = MandelbrotMath.translate( -width / 2.0f, -height / 2.0f, 0 ); float[] screenAdj = MandelbrotMath.multiply( screenScale, screenTrans ); float[] finalMat = MandelbrotMath.multiply( view, screenAdj ); // 2. Render Setup OpenGL.glUseProgram( shaderProgram ); OpenGL.glUniformMatrix4fv( u_matrix_location, 1, false, finalMat ); // 3. Apply Time float timeToSend = cycling ? ( SDL2.SDL_GetTicks( ) / 1000.0f ) : 3.0f; OpenGL.glUniform1f( u_time_location, timeToSend ); // 4. Apply Dynamic Iterations double zoomRatio = Math.max( 1.0, zoom / 300.0 ); int dynamic_iter = 500 + (int)( 150.0 * Math.log10( zoomRatio ) ); if (dynamic_iter > 1000) dynamic_iter = 1000; OpenGL.glUniform1i( u_max_iter_location, dynamic_iter ); // Print zoom and iterations if they changed this frame if ( zoom != lastZoom ) { Stream.writeLine( "Zoom: " + Double.toString( zoom ) + " | Iterations: " + Integer.toString( dynamic_iter ) ); lastZoom = zoom; } // 5. Draw OpenGL.glClearColor( 0.0f, 0.0f, 0.0f, 1.0f ); OpenGL.glClear( OpenGL.GL_COLOR_BUFFER_BIT ); OpenGL.glBindBuffer( OpenGL.GL_ARRAY_BUFFER, vbos[ 0 ] ); OpenGL.glVertexAttribPointer( 0, 3, OpenGL.GL_FLOAT, false, 0, 0 ); OpenGL.glEnableVertexAttribArray( 0 ); OpenGL.glDrawArrays( OpenGL.GL_TRIANGLE_STRIP, 0, 4 ); OpenGL.glDisableVertexAttribArray( 0 ); if ( screenshot_requested ) { performScreenshot( ); } SDL2.SDL_GL_SwapWindow( w ); } // --- Screenshot Logic --- private static void performScreenshot( ) { Stream.writeLine( "Screenshot requested." ); byte[] pixels = new byte[ width * height * 4 ]; OpenGL.glReadPixels( 0, 0, width, height, OpenGL.GL_RGBA, OpenGL.GL_UNSIGNED_BYTE, pixels ); String file = "mandelbrot_" + Time.toString( ) + ".png"; if ( emscriptenActive ) { Emscripten.saveImage( width, height, pixels, file, true ); Stream.writeLine( "Screenshot saved: " + file ); } else { bool success = SDL2.savePng( file, width, height, 4, pixels, width * 4, true ); if ( success ) { Stream.writeLine( "Screenshot saved: " + file ); } else { Stream.writeLine( "Failed to save screenshot." ); } } screenshot_requested = false; } // --- Window & Fullscreen Logic --- public static bool fullScreenModeChange( int eventType, EmscriptenFullScreenChangeEvent e, Object userObject ) { if ( displayMode == DisplayMode.FullWindow ) return true; if ( e.isFullScreen ) { width = e.elementWidth; height = e.elementHeight; } else { width = initialWidth; height = initialHeight; } Emscripten.setCanvasSize( width, height ); SDL2.SDL_SetWindowSize( w, width, height ); OpenGL.glViewport( 0, 0, width, height ); if ( e.isFullScreen ) displayMode = DisplayMode.FullScreen; else displayMode = emscriptenActive ? DisplayMode.Canvas : DisplayMode.FullWindow; return true; } public static bool resizeEventCallback( int eventType, EmscriptenUiEvent uiEvent, Object userObject ) { if ( displayMode == DisplayMode.FullWindow ) { width = uiEvent.windowInnerWidth; height = uiEvent.windowInnerHeight; Emscripten.setCanvasSize( width, height ); SDL2.SDL_SetWindowSize( w, width, height ); OpenGL.glViewport( 0, 0, width, height ); } return true; } public static void toggleFullScreenMode( ) { if ( emscriptenActive ) { if ( displayMode == DisplayMode.FullScreen ) Emscripten.exitFullScreenMode( ); else Emscripten.enterFullScreenMode( ); } else { bool fullScreenMode = ( SDL2.SDL_GetWindowFlags( w ) & SDL2.SDL_WINDOW_FULLSCREEN ) != 0; if ( !fullScreenMode ) prevWindowSize = SDL2.SDL_GetWindowSize( w ); fullScreenMode = !fullScreenMode; SDL2.SDL_SetWindowFullscreen( w, fullScreenMode ? SDL2.SDL_WINDOW_FULLSCREEN : 0 ); if ( fullScreenMode ) { int displayIndex = SDL2.SDL_GetWindowDisplayIndex( w ); SDL_DisplayMode dm = SDL2.SDL_GetDesktopDisplayMode( displayIndex ); if ( dm == null ) return; width = dm.w; height = dm.h; } else { width = prevWindowSize.w; height = prevWindowSize.h; } SDL2.SDL_SetWindowSize( w, width, height ); OpenGL.glViewport( 0, 0, width, height ); if ( fullScreenMode ) displayMode = DisplayMode.FullScreen; else displayMode = emscriptenActive ? DisplayMode.Canvas : DisplayMode.FullWindow; } } public static void toggleFullWindowMode( ) { if ( displayMode == DisplayMode.FullWindow ) { width = prevWidth; height = prevHeight; SDL2.SDL_SetWindowSize( w, width, height ); OpenGL.glViewport( 0, 0, width, height ); Emscripten.exitFullWindowMode( ); displayMode = DisplayMode.Canvas; } else { prevWidth = width; prevHeight = height; width = Emscripten.getClientWidth( ); height = Emscripten.getClientHeight( ); Emscripten.enterFullWindowMode( ); SDL2.SDL_SetWindowSize( w, width, height ); OpenGL.glViewport( 0, 0, width, height ); displayMode = DisplayMode.FullWindow; } } } // --- Custom Matrix Math --- class MandelbrotMath { public static float[] identity( ) { float[] m = { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; return m; } public static float[] scale( float x, float y, float z ) { float[] m = identity( ); m[ 0 ] = x; m[ 5 ] = y; m[ 10 ] = z; return m; } public static float[] translate( double x, double y, double z ) { float[] m = identity( ); m[ 12 ] = (float)x; m[ 13 ] = (float)y; m[ 14 ] = (float)z; return m; } public static float[] multiply( float[] a, float[] b ) { float[] res = new float[ 16 ]; for ( int i = 0; i < 16; ++i ) res[ i ] = 0; for ( int i = 0; i < 4; i++ ) { for ( int j = 0; j < 4; j++ ) { for ( int k = 0; k < 4; k++ ) { res[ i + j * 4 ] += a[ i + k * 4 ] * b[ k + j * 4 ]; } } } return res; } }